// Contains code copied from istanbul-lib-report (https://github.com/istanbuljs/istanbuljs/tree/main/packages/istanbul-lib-report).
// The link to the original license is in the VENDORED.md file in the parent directory.

/*
 Copyright 2012-2015, Yahoo Inc.
 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
 */
"use strict";

const coverage = require("../../istanbul-lib-coverage/index.cjs");
const Path = require("./path.cjs");
const { BaseNode, BaseTree } = require("./tree.cjs");

class ReportNode extends BaseNode {
  constructor(path, fileCoverage) {
    super();

    this.path = path;
    this.parent = null;
    this.fileCoverage = fileCoverage;
    this.children = [];
  }

  static createRoot(children) {
    const root = new ReportNode(new Path([]));

    children.forEach((child) => {
      root.addChild(child);
    });

    return root;
  }

  addChild(child) {
    child.parent = this;
    this.children.push(child);
  }

  asRelative(p) {
    if (p.substring(0, 1) === "/") {
      return p.substring(1);
    }
    return p;
  }

  getQualifiedName() {
    return this.asRelative(this.path.toString());
  }

  getRelativeName() {
    const parent = this.getParent();
    const myPath = this.path;
    let relPath;
    let i;
    const parentPath = parent ? parent.path : new Path([]);
    if (parentPath.ancestorOf(myPath)) {
      relPath = new Path(myPath.elements());
      for (i = 0; i < parentPath.length; i += 1) {
        relPath.shift();
      }
      return this.asRelative(relPath.toString());
    }
    return this.asRelative(this.path.toString());
  }

  getParent() {
    return this.parent;
  }

  getChildren() {
    return this.children;
  }

  isSummary() {
    return !this.fileCoverage;
  }

  getFileCoverage() {
    return this.fileCoverage;
  }

  getCoverageSummary(filesOnly) {
    const cacheProp = `c_${filesOnly ? "files" : "full"}`;
    let summary;

    if (Object.prototype.hasOwnProperty.call(this, cacheProp)) {
      return this[cacheProp];
    }

    if (!this.isSummary()) {
      summary = this.getFileCoverage().toSummary();
    } else {
      let count = 0;
      summary = coverage.createCoverageSummary();
      this.getChildren().forEach((child) => {
        if (filesOnly && child.isSummary()) {
          return;
        }
        count += 1;
        summary.merge(child.getCoverageSummary(filesOnly));
      });
      if (count === 0 && filesOnly) {
        summary = null;
      }
    }
    this[cacheProp] = summary;
    return summary;
  }
}

class ReportTree extends BaseTree {
  constructor(root, childPrefix) {
    super(root);

    const maybePrefix = (node) => {
      if (childPrefix && !node.isRoot()) {
        node.path.unshift(childPrefix);
      }
    };
    this.visit({
      onDetail: maybePrefix,
      onSummary(node) {
        maybePrefix(node);
        node.children.sort((a, b) => {
          const astr = a.path.toString();
          const bstr = b.path.toString();
          return astr < bstr
            ? -1
            : astr > bstr
              ? 1
              : /* istanbul ignore next */ 0;
        });
      },
    });
  }
}

function findCommonParent(paths) {
  return paths.reduce(
    (common, path) => common.commonPrefixPath(path),
    paths[0] || new Path([]),
  );
}

function findOrCreateParent(parentPath, nodeMap, created = () => {}) {
  let parent = nodeMap[parentPath.toString()];

  if (!parent) {
    parent = new ReportNode(parentPath);
    nodeMap[parentPath.toString()] = parent;
    created(parentPath, parent);
  }

  return parent;
}

function toDirParents(list) {
  const nodeMap = Object.create(null);
  list.forEach((o) => {
    const parent = findOrCreateParent(o.path.parent(), nodeMap);
    parent.addChild(new ReportNode(o.path, o.fileCoverage));
  });

  return Object.values(nodeMap);
}

function addAllPaths(topPaths, nodeMap, path, node) {
  const parent = findOrCreateParent(
    path.parent(),
    nodeMap,
    (parentPath, parent) => {
      if (parentPath.hasParent()) {
        addAllPaths(topPaths, nodeMap, parentPath, parent);
      } else {
        topPaths.push(parent);
      }
    },
  );

  parent.addChild(node);
}

function foldIntoOneDir(node, parent) {
  const { children } = node;
  if (children.length === 1 && !children[0].fileCoverage) {
    children[0].parent = parent;
    return foldIntoOneDir(children[0], parent);
  }
  node.children = children.map((child) => foldIntoOneDir(child, node));
  return node;
}

function pkgSummaryPrefix(dirParents, commonParent) {
  if (!dirParents.some((dp) => dp.path.length === 0)) {
    return;
  }

  if (commonParent.length === 0) {
    return "root";
  }

  return commonParent.name();
}

class SummarizerFactory {
  constructor(coverageMap, defaultSummarizer = "pkg") {
    this._coverageMap = coverageMap;
    this._defaultSummarizer = defaultSummarizer;
    this._initialList = coverageMap.files().map((filePath) => ({
      filePath,
      path: new Path(filePath),
      fileCoverage: coverageMap.fileCoverageFor(filePath),
    }));
    this._commonParent = findCommonParent(
      this._initialList.map((o) => o.path.parent()),
    );
    if (this._commonParent.length > 0) {
      this._initialList.forEach((o) => {
        o.path.splice(0, this._commonParent.length);
      });
    }
  }

  get defaultSummarizer() {
    return this[this._defaultSummarizer];
  }

  get flat() {
    if (!this._flat) {
      this._flat = new ReportTree(
        ReportNode.createRoot(
          this._initialList.map(
            (node) => new ReportNode(node.path, node.fileCoverage),
          ),
        ),
      );
    }

    return this._flat;
  }

  _createPkg() {
    const dirParents = toDirParents(this._initialList);
    if (dirParents.length === 1) {
      return new ReportTree(dirParents[0]);
    }

    return new ReportTree(
      ReportNode.createRoot(dirParents),
      pkgSummaryPrefix(dirParents, this._commonParent),
    );
  }

  get pkg() {
    if (!this._pkg) {
      this._pkg = this._createPkg();
    }

    return this._pkg;
  }

  _createNested() {
    const nodeMap = Object.create(null);
    const topPaths = [];
    this._initialList.forEach((o) => {
      const node = new ReportNode(o.path, o.fileCoverage);
      addAllPaths(topPaths, nodeMap, o.path, node);
    });

    const topNodes = topPaths.map((node) => foldIntoOneDir(node));
    if (topNodes.length === 1) {
      return new ReportTree(topNodes[0]);
    }

    return new ReportTree(ReportNode.createRoot(topNodes));
  }

  get nested() {
    if (!this._nested) {
      this._nested = this._createNested();
    }

    return this._nested;
  }
}

module.exports = SummarizerFactory;
